• C++面向对象(一)


    0.前言:

    C++中结构体不可以有成员函数。
    【结构体大小计算回忆】
    请添加图片描述
    在64位下,指针大小为8字节。
    a1:4,
    a2:4
    a3:4
    指针:8
    sum:20 => 24,因为需要成8的倍数。【对齐数】

    1. 构造函数

    • 由来:C语言中struct经常忘记初始化:像栈一样给top位置和栈size
      所以C++加了构造函数,会自动初始化。
    • 特殊点:如果自己不手动写,编译器自己实现。对象实例化时自动调用。
    • 特点:
      最好写成public:类型。
      此外,C++成员变量命名风格喜欢在变量前加_

    1.1带缺省的构造函数

      如下图,缺省和无参不可以同时存在,语法上无参和全缺省可以同时存在,因为构成了函数重载。因为如果创建Date d1;这样有二异性。它不知道调用缺省构造函数还是无参构造函数。
    请添加图片描述

    • 改进:
      请添加图片描述
        也可以只传一两个【推荐全缺省或半缺省】

    1.2 默认构造函数

    • 默认构造函数:特指编译器自动生成的无参构造函数,虽然编译器创建了,但是查看值,并没有被初始化。
      请添加图片描述
         1. 那么默认生成的是不是什么事情都没干?
         答:C++里面把类型分为两类:
      内置类型(基本类型如int、char、double、内置类型数组等),自定义类型【结构体、对象】。
      对于自定义类型的成员变量,它编译器会自己去调用该类型的构造函数。而内置类型,就不管。所以如下year、month、day,仍然是随机值。请添加图片描述
    • 注意点:
         如果你写了一个类的有参构造函数,那么系统将不提供无参构造函数,但是你最好写出来,不然如果你创建对象不给参数,就会报错。因为上面说了,系统默认给无参构造时是你自己啥构造函数都不写。为了避免错误,最好自己手动添加。**最建议你直接写一个全缺省的构造函数。**此外,如果是自定义类型有你手写的构造函数,它会自己去调用,如果你自己没写,它还会又深入去如上的A类中创建无参构造函数,但是A类中的a因为是int类型属于内置类型,又不会被初始化值。
      暴露了【C++缺点】:没有对内置类型和自定义类型统一处理。

    此外注意:创建对象不给参数时,直接用对象名,不加括号。

    2. 析构函数:资源释放

       不是完成对象的销毁,为了完成对象中的一些资源清理工作。比如:对指针在堆上申请的空间做释放。此外,一个类只有一个析构函数。
    ~Date()
    {
    //释放指针类资源,比如栈中a指针,会申请一片连续空间。;
    free(a);
    // 变量清0、该置0的置0
    }

    • 编译器生成的默认析构函数和默认构造一样,自定义的类型会去调用它本身的析构函数【就是你类中的类属性,默认析构调用它的析构】。而内置类型就不会自动删除。

    • 虽然C++有缺点,但是也方便。方便就方便在类中的类属性成员变量,会自己直接调用类属性它的类的默认构造参数,直接就做完赋值了,比较方便。
      总结:没有资源需要清理,不用自己实现析构函数,其中对于自定义类型会调用该自定义类型的析构,内置类型不做处理。

    3. this指针

    • 特点:
    1. this指针的类型: 类类型* const
    2. 只能在成员函数内部使用
    3. 对象中不存储this指针,this指针只是个“成员函数”的形参。当对象调用成员函数时,编译器自动把对象地址作为实参给了this形参,this形参实际是第一个成员函数的参数,它是隐藏起来的。所以对象中不存储this指针,且没必要存。
    4. 【注意:】可以为空,当你不输出或不使用对象成员变量时,显然可以不穿地址,所以this没必要接收。

    4. 构造函数和析构函数的调用顺序:

    请添加图片描述

    • 最先构造的,最晚析构,结合栈特点。
      所以以上选B。
    • 再看一个:
      请添加图片描述
      解析:
    1. 全局的C和static D,都存在静态内存区,在这个静态区内,也得符合栈顺序。
    2. A是在堆上申请开,和局部静态变量是同级的,A语句早于C语句,所以A先掉构造。然后是C,再D。且A、C都在数据段,这两个析构顺序符合栈特点
      最后析构顺序按照构造顺序相反。

    请添加图片描述
    请添加图片描述

    5. 拷贝构造函数:复制对象

    • 特点:
    1. 拷贝构造函数与构造函数构成重载。
    2. 使用同类型的对象值去拷贝,加const防改变且用别名&防无限递归调用。简言之,参数类型MyClass必须加&
        解释2: 对象类型的参数都是临时变量,临时拷贝出来的,编译器自动调用拷贝构造函数。所以如果拷贝构造的参数是类,会无限递归。,所以一定记住:参数类型MyClass必须加&,也就是类的引用。
    • 使用场景:
      自定义类型用同类型对象做初始化,就是拷贝构造
    • 注意:
        1. 因为参数用了const&,起来别名,防止赋值写反导致先前对象被误改,所以习惯最前面加const。
         2. 如果没有显示定义,系统会生成默认的拷贝构造函数。
    1. 内置类型成员:也会完成按字节序的拷贝。:即按一个个字节去拷贝。按字节依次去拷贝。
    • 思考:
        那么要不要写拷贝构造,因为发现默认生成的也很够用。是不是都不需要写?
         答:不可以, 比如栈就不行,因为会做浅拷贝,栈2用栈1去拷贝复制,结果栈2析构,会把自己指向的空间释放,但是栈1析构已经释放了。所以浅拷贝导致它们指向的空间被析构两次,同一片空间,不能同时释放两次。且s2删、增,也会影响s2。所以需要深拷贝,而深拷贝需要我们自己写。
      但是对于日期Date这样的类可以用。但是栈不可以用。
      拷贝构造对内置类型,都只是做值拷贝或浅拷贝。有指向资源的【或说自定义类型的成员】,都不可以用系统默认生成的拷贝构造,且系统遇到了自定义类型的属性,会调用这个自定义成员属性它自己【自定义类型】的拷贝构造函数。(且被调用的类的拷贝构造函数一定是我们人为实现的,因为需要深拷贝的拷贝构造函数)。

    【总结】:系统会自动生成默认拷贝构造函数,系统会对内置类型和自定义类型都会拷贝处理。但是对两者处理细节不一样,且和构造、析构都不一样。
    特点

    1. 构造函数,它就是为了把当前对象的值做成和参数一样的对象。所以没有返回值
    2. 只有一个参数。是对象的引用
    3. 每个类都存在 拷贝构造函数,分浅拷贝和深拷贝。默认的拷贝构造函数是浅拷贝,会拷贝参数对象中同样的地址,所以有时候可能会影响一些操作,一个对象的改变影响到这个浅拷贝的对象,比如你改变值,或析构另外一个对象。深拷贝要自己写,且有时候必须要用深拷贝。

    拷贝构造使用的代码举例

    // 法1
    class A1(2,1);
    class A2(A1);
    // 法2:
    class A3 = A1;
    // 调用
    A3 = A2; 这是在
    

    而最下面那一行,已经有了对象了,那个涉及的是赋值,而不是拷贝构造,且对象类型的右值,编译器会自动做=运算符重载。

    1. 函数返回值是对象时会返回临时变量,做一次拷贝构造。
    2. 形参是类时。

    【回忆】:
    ** 插入:【size_t】类型是什么**
    size_t:数组下标,无符号整数类型。
    无符号不能保存负数,最低存到-1。

    const修饰成员函数

    • 特点:
    1. 常函数:成员函数后面加const后称这个函数为常函数。如下:
      void Person::show() const
    2. 常函数内不可以修改成员属性。即不可以出现类似如下:
      void fun(){ this->a = val}; 这种改变成员变量值的赋值语句绝不可以。
    3. 成员变量声明加mutable,在常函数中依然可以修改。
    4. 声明和实现时,都需要加const。

    【构造函数的初始化列表】

    在调用构造函数之前,先会默认做初始化列表操作,这会才是在做定义。即开辟空间。而后的自动调用的构造函数,是在做初始化。

    • 使用场景:
    1. const 成员变量,因为常量必须在定义时初始化,之后再也不能改变。
    2. 引用成员变量
    3. 自定义类型的成员变量:不使用初始化列表会调用多次构造函数。
    • 注意:
      类中变量声明次序就是初始化列表中的初始化顺序。

    • 例题:类成员声明顺序就是构造化顺序。
      请添加图片描述
      因为a2声明先于a1,而a2构造列表中需要a1值,但是a1还没有,所以a2是随机值。a1是1。

    • 例题:
      请添加图片描述
      静态变量不是常量,静态变量必须在类外初始化,而const常量才是在初始化列表,所以

    【回忆】变量的声明、定义、初始化

    1. 声明:声明变量a,不是定义,不给a分配内存空间。如下两种算声明。一般是带着extern的都叫声明。
      还有场景:函数在.h文件中做声明,而在.c文件中做定义和初始化。
    extern int a; // 声明
    在类中,写出类的成员变量和有哪些函数【不去实现】,也是声明。
    
    1. 定义:特指指给分配了空间。如下代码算声明a,也给a定义了。
    int a; 
    
    1. 初始化:给初始值。

    const修饰成员变量

    特点

    1. 必须使用构造函数初始化列表方式做初始化,因为初始化列表处是在做定义的地方,即:在开辟空间了,而构造函数内部:在做初始化,即空间之前已经开辟好了。【要知道创建类在调用构造函数之前,先会默认做初始化列表操作,这会才是在做定义。即开辟空间】
    2. const成员变量需要在定义时就初始化。所以const成员变量必须在初始化列表时赋值。

    const修饰对象

    1. const对象不能调用普通类的成员方法。
      请添加图片描述
        如上,对象调用成员函数,会传地址给隐藏的第一个形参this,而this的类型是:Date* const this,这代表着this指向的对象不能改变。在d1.Print()时,传&d1给this,Date传给 Date const this,没有问题,这只是保护起来了。
        但是d2.Print()传&d2,它的类型是: const Date*,在const后面,即指向的内容的成员变量也不能改变。即d2对象的成员变量只读,但是传给this后,this可以改变对象成员,是权限放大了,所以不可以。**造成d2不可以调用Print(),所以这里得使用const修饰成员函数。即之前的const成员变量。但是this是隐含的,不能显示加,所以我们直接加给()的后面,这样就不能改变*this所指向的对象的成员变量。**

    这里注意,在后面的const,或const一行没有,不涉及权限缩小。比如上面const Date this就是,且所有this指针,前面的const都是保护this不变,没有权限缩小。如下也是。*

    const int x = a;
    
    • 例题:
      请添加图片描述
    • 解析:
      这是一个const成员函数,对象值不能改变,所以函数调用后,对象值无变化。

    运算符重载的应用: 实现Date:

    class Date
    
    {
    
    public:
    
    // 获取某年某月的天数
    
    int GetMonthDay(int year, int month);
    
    
    
      // 全缺省的构造函数
    
    Date(int year = 1900, int month = 1, int day = 1);
    
    
    
      // 拷贝构造函数
    
    // d2(d1)
    
    Date(const Date& d);
    
       
    
      // 赋值运算符重载
    
    // d2 = d3 -> d2.operator=(&d2, d3)
    
    Date& operator=(const Date& d);
    
    
    
      // 析构函数
    
    ~Date();
    
    
    
      // 日期+=天数
    
    Date& operator+=(int day);
    
    
    
      // 日期+天数
    
    Date operator+(int day);
    
    
    
      // 日期-天数
    
    Date operator-(int day);
    
    
    
       // 日期-=天数
    
    Date& operator-=(int day);
    
    
    
      // 前置++
    
    Date& operator++();
    
    
    
      // 后置++
    
    Date operator++(int);
    
    
    
      // 后置--
    
    Date operator--(int);
    
    
    
      // 前置--
    
    Date& operator--();
    
    
    
      // >运算符重载
    
    bool operator>(const Date& d);
    
    
    
      // ==运算符重载
    
    bool operator==(const Date& d);
    
    
    
      // >=运算符重载
    
    bool operator >= (const Date& d);
    
       
    
      // <运算符重载
    
    bool operator < (const Date& d);
    
    
    
       // <=运算符重载
    
    bool operator <= (const Date& d);
    
    
    
      // !=运算符重载
    
    bool operator != (const Date& d);
    
    
    
      // 日期-日期 返回天数
    
    int operator-(const Date& d);
    
    private:
    
    int _year;
    
    int _month;
    
    int _day;
    
    };
    
    1. 重载">“等比较运算符,这个建议从”=="开始,然后互相使用,减少代码的重复率。利用已有的重载运算符,去简写其它的。
    2. 重载"+“和”+=" 、 “-“和”-=”。要注意区分"+=“和”+",+和-不需要变化自己,而+=和-=需要。
      +=:日期加天数:天数超这个月就一直加。月,然后月超12就加年。
      且最后返回的是当前值,所以函数返回值是引用类型、返回*this。
      +:本身不能加day,所以用临时变量。而**用临时变量,就别引用。**用了引用就出错了,因为那个date不存在了。
    Date& Date::operator+=(int day)
    {
    	_day += day;
    	int tmp = (*this).GetMonthDay(_year, _month);
    	while (_day > tmp)
    	{
    		_day -= tmp;
    		_month++;
    		if (_month > 12)
    		{
    			_month = 1;
    			_year++;
    		}
    		tmp = (*this).GetMonthDay(_year, _month);
    	}
    	return *this; 
    }
    
    
    Date Date::operator+(int day)
    {
    	Date d1(*this);
    	d1._day += day;
    	return d1;
    }
    
    1. 重载++:分前置和后置:写法上为了区分:后置加了默认参数int,写为:operator++(int)。创始人主要是为了加个参数做区分。编译的时编译器会多传一个0。
      其中:前置++,要返回自己,所以用引用类型传即可。
      而后置++,应该返回自己的原来,所以用拷贝的临时变量,所以返回普通类类型。
      分析:前置不使用拷贝构造函数。而后置++用两次拷贝构造,所以推荐用前置++。
    Date& Date::operator++()
    {
    	*this += 1;
    	return *this;
    }
    
    Date& Date::operator++(int)
    {
    	Date d = *this;
    	*this += 1;
    	return d;
    }
    
    
    1. 重载"-“、”-=":
      -、-=
      这里调用GetMonthDay时写_year、_month就好,加*this也行,但是难看。

    2. 两个日期天数之差
      有以下几种思路:
      a. 求和公元0年第1天,再作差。
      b. 让小日期加到大日期

    运算符重载

    • 为了增强代码可读性,让自定义类型对象支持运算符。这也是STL的关键利器,此外: “::”、“.*”、“?:”、"."不可以重载。
      都是需要自己重载。
    • 应用:
      比如:日期对象之间做加减。
    • 注意:
    1. 返回值类型需要自己考虑,三操作数不允许重载,一般是单、双。
    2. 有几个操作数,就有几个参数。(建议传引用)因为用拷贝构造,对象类型参数必须用引用。否则无限递归且加const使得更安全,如果下面写错了会报错。
    3. 运算符重载上要使用成员变量,而成员变量总是私有那么如何提供?
      1. private变public
      2. 造get方法
      3. 友元:比public好,但会稍破坏封装。
      4. 成员函数法:把opeartor放到类中,参数减少一个,使opeartor变为成员函数。
        变成成员函数后,函数多了个 隐藏参数:this指针。
    • 使用:
      cout时候要注意:
      cout<< d1 > d2 << endl;
      这样要小心流顺序,<<优先级高于>,所以为了他俩能计算,把d1和d2括起来。
    • d1>d2,编译器会自动转为:operator>(d1, d2) ,但是为什么不支持直接自写:opeartor(>d1,d2)?
      答:开头说了:为了可读性。显然,直接写符号更易懂看着舒服。

    :: 、sizeof、? . 这四个不要重载。 .*也不能重载。其中点用来做对象访问成员的。
    C++必须写返回值类型。

    全局和类上都写,会不会重定义:即非要在外面写operator以">"为例,写两个参数,不会编译报错,因为构成重载。一般建议写成员中的。类中的函数,不是属于全局,属于类域。且大部分、习惯上, 都把运算符重载放在类中,所以优先去类中找这也寻找本身更有优势。
    最后发现调用成员,优先去成员中找。

    赋值运算符重载:

    使用场景:比如日期类Date,想让Date d1和d3相等。实现opeartor赋值。
    不需要有返回值。

    • 为什么使用引用类型返回就会减少拷贝构造函数的调用?
      比如: d1=d2=d3,如果此时使用如下操作符重载,会调用两次拷贝构造函数。
      前情概要:对象变量做返回值,会调用拷贝构造函数,不会返回某个对象它本身,除非用别名&引用,才返回这个东西,且不会调用拷贝构造函数
      前情概要:对象变量做返回值,会调用拷贝构造函数,不会返回某个对象它本身
      前情概要:对象变量做返回值,会调用拷贝构造函数,不会返回某个对象它本身
      Date operator=(const Date & d)
      	{
      		_year = d._year;
      		_month = d._month;
      		_day = d._day;
      
      		return *this;
      	}
      

    请添加图片描述
    因为返回了一个Date类型对象,是临时的,它本质通过了拷贝构造。所以调用拷贝构造在返回时。

    而使用引用做返回,则一次都不使用。
    
    Date& operator=(const Date & d)
    	{
    		_year = d._year;
    		_month = d._month;
    		_day = d._day;
    
    		return *this;
    	}
    

    请添加图片描述
    当重载等号以引用返回,返回的是已经存在的对象,一直用的是等号重载。

    此外,极端情况自己给自己赋值,就可以不用处理了,直接判断后跳过。
    且用地址比较,不要去比较对象。
    this指针是地址,&d也是地址。如果y用:*this!=d,还要对!=做重载,因为两边是两个对象, 不能直接比。

    此外,不写赋值运算符重载,编译器会自己生成一个。
    此外,不写赋值运算符重载,编译器会自己生成一个。
    此外,不写赋值运算符重载,编译器会自己生成一个。
    编译器默认生成的赋值重载,根拷贝构造做的事情类似。

    1. 内置类型成员,会完成字节序值拷贝——浅拷贝。
    2. 自定义类型成员,会调用自定义类型的=重载。

    如上,如果是队列用两个栈实现,那么这个队列不需要自己实现赋值重载,因为内含Stack拥有,且它自己没有一些连续内存的地址空间。但是如果栈本身,就需要自己实现赋值重载,因为这个自定义类型中使用了指针,需要手动释放。
    总结,默认生成的四个默认成员函数,构造和析构处理机制是类似的。
    拷贝构造和赋值重载处理机制是基本类似的。

    • 注意点:

    Date d5 = d1;是拷贝构造,不是赋值重载。

    匿名对象
    比如:创建一个对象只是为了传参。

    一般对象声明周期在当前函数。
    匿名对象生命周期只在当前这一行。

    如果直接传匿名对象,拷贝构造会少调用一次。

    【取地址&运算符重载】

    默认成员函数:不写,也可以对这是取到地址,所以说它是默认重载的。

    赋值运算符重载

    1. 默认运算符“=”只是实现了"浅层复制"功能。
    2. 重载赋值运算符
    3. 赋值=运算符重载只能在类内重载

    下列关于赋值运算符“=”重载的叙述中,正确的是( )
    A.赋值运算符只能作为类的成员函数重载
    B.默认的赋值运算符实现了“深层复制”功能
    C.重载的赋值运算符函数有两个本类对象作为形参
    D.如果己经定义了复制拷贝构造函数,就不能重载赋值运算符
    根据上面引用,知道A对,BC错,而D:可以有拷贝构造,也可以重载赋值=运算符。

    重载运算符:流插入<<和提取>>【友元中】

    流重载是:双操作数的操作符重载
    本质是:对象对 流对象的操作。
    所以有两个操作数,必须第一个是操作数,第二个参数是右操作数。

    所以不能重载成重载函数。
    如果还想cout到左边,不能实现成员函数,所以就做一个全局,不是成员函数,就不区分左右了,因为成员函数,第一个必须是操作数,默认是类类型的,也就是那个对象。
    【改进】:写全局运算符重载。不写类运算符重载。

    • 【面试题】:
      哈希冲突如何解决?
      散列法、双重哈希等等。
  • 相关阅读:
    pycharm 断点调试python Flask
    音频采集原理
    Django重定向类HttpResponseRedirect、HttpResponsePermanentRedirect和重定向函数redirect
    【Kafka】KafkaTopic命令
    hosts文件的使用以及修改
    Kubernetes(k8s)是什么?解决了哪些问题?
    Spring IoC和DI详解
    Java之集合(15个demo)
    【docker】Docker镜像管理
    【云原生】springcloud12——服务网关Gateway
  • 原文地址:https://blog.csdn.net/myscratch/article/details/126999056