2 类和对象 -- 学习笔记

目录:

一.面向对象的思想,定义格式,class和struct的比较

  1. 在C++中,struct可以定义类,将struct的功能做了一个提升,不仅可以定义数据,也可以定义函数。

  2. class与struct之间的区别:默认的访问权限不一样,struct默认访问权限是public,class默认访问权限是private。

  3. 在类中定义的成员函数,都是inline函数。除了可以在类内部实现外,成员函数还可以在类之外实现。在类定义的外部定义成员函数时,应使用作用域限定符(::)来标识函数所属的类。

  4. 代码规范:形成自己的风格


二.对象的创建与销毁

  1. 构造函数:

    1. 构造函数特点:与类名相同、没有返回类型,void都没有;

    2. 对象的创建会自动调用构造函数,完成数据成员的初始化;

    3. 默认情况下编译器可以自动生成一个无参构造函数;

    4. 如果自己写了构造函数,那编译器不会再主动提供,如果还想继续使用无参的,必须显示定义,从而得到构造函数是可以重载的特点。。

  2. 初始化列表:

    1. 真正初始化数据成员的位置是初始化列表或者叫初始化表达式。记住:数据成员初始化的顺序,与其在初始化列表中的顺序没有关系,只与数据被声明时候的顺序有关。在构造函数里的是赋值操作,不是初始化操作。

    2. 如果没有在构造函数的初始化列表中显式地初始化成员,则该成员将在构造函数体之前执行默认初始化,在函数体中执行赋值操作。

  3. 析构函数:

    1. 析构函数的特点:与类名相同,但是前面加一个取反符号,没有返回类型,void都没有,并且没有参数,所以具有唯一性,不能重载。
    2. 对象在离开其作用域的时候,会自动调用析构函数,完成数据成员的清理工作。
    3. 默认情况下,编译器会主动生成一个析构函数。并且注意,析构函数是可以显示调用的,但是不建议,可能会出现问题。
  4. 堆上的对象不会自动调用析构函数,需要delete

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

    1. 对于全局定义的对象,每当程序开始运行,在主函数main接受程序控制权之前,就调用构造函数创建全局对象,整个程序结束时,自动调用全局对象的析构函数。
    2. 对于局部定义的对象,每当程序流程到达该对象的定义处就调用构造函数,在程序离开局部对象的作用域时调用对象的析构函数。
    3. 对于关键字static定义的静态局部变量,当程序流程第一次到达该对象定义处调用构造函数,在整个程序结束时调用析构函数。
    4. 对于用new运算符创建的对象,每当创建该对象时调用构造函数,当用delete删除该对象时,调用析构函数。

三.拷贝构造函数

  1. 深拷贝与浅拷贝

    1. 浅拷贝:指针的拷贝
    2. 深拷贝:不仅拷贝值,还进行堆空间的申请
  2. 拷贝构造函数的调用时机

    1. 当用一个已经存在的对象初始化另一个新对象时,会调用拷贝构造函数。
    2. 当实参和形参都是对象,进行实参与形参的结合时,会调用拷贝构造函数。
    3. 函数返回值是对象,函数调用完成后返回(要关闭优化选项,否则不能看到拷贝构造函数的调用过程:编译时在后面加 -fno-elide-constructors) [这里用值去接收时可能会有两次拷贝构造,return时拷贝构造生成临时对象,若接收的值是第一次声明的,那么用该临时对象再拷贝构造一次接收的对象]
    4. 临时对象/匿名对象生命周期只在本行,该行执行结束后就立刻销毁。
  3. 拷贝构造函数例如Point(const Point &rhs)的引用与const可以去掉吗?

    1. 如果去掉引用符号,形参是对象,会无穷递归调用拷贝构造函数

    2. const不能去掉,非const左值引用不能绑定右值,当传递右值(临时对象),无法调用拷贝构造函数。左值:可以进行取地址的就是左值

      右值:不能进行取地址,临时对象、匿名对象都是右值,字面值常量(10)也是右值


四.this指针

实质:指向对象本身

位置:隐含在每一个非静态成员函数的第一个参数的位置

形式:类类型 * const this,例如:Point * const this;

​ 对于类成员函数而言,并不是一个对象对应一个单独的成员函数体,而是此类的所有对象共用这个成员函数体。 当程序被编译之后,此成员函数地址即已确定。而成员函数之所以能把属于此类的各个对象的数据区别开, 就是靠这个this指针。函数体内所有对类数据成员的访问,都会被转化为this->数据成员的方式。


五.赋值运算符函数

  1. 四部曲:

    1. 自复制 // 防止自己复制给自己,导致第2步出错
    2. 释放左操作数 //不释放,可能导致内存泄漏
    3. 深拷贝 //如果直接拷贝,可能因为右操作数比左操作数空间大,而内存溢出了
    4. 返回*this //返回对象本身
  2. 赋值运算符函数的函数参数和返回值问题

    1. 赋值运算符函数的参数中引用可以去掉吗?不能,会多调用一个拷贝构造函数,影响效率
    2. 赋值运算符函数的参数中const可以去掉吗?当赋值时,右操作数是右值时候,识别不了,非const左值引用不能绑定右值
    3. 赋值运算符函数的返回类型中的引用可以去掉吗?符合拷贝构造函数调用时机3,多调用一次拷贝构造函数,影响效率
    4. 赋值运算符函数的返回类型可以不是类类型吗?不能,连等
  3. 举例:

    Computer &Computer::operator=(const Computer &rhs)
    {
        cout << "Computer &operator=(const Computer &)" << endl;
        if(this != &rhs)//1、自复制
        {
            delete [] _brand;//2、释放左操作数(防止内存泄漏)
            _brand = nullptr;
            
            _brand = new char[strlen(rhs._brand) + 1]();//3、深拷贝
            strcpy(_brand, rhs._brand);
            _price = rhs._price;
        }
    
        return *this;//4、返回*this
    }
    

六.特殊数据成员初始化

  1. 常量数据成员

    必须要在初始化列表中进行初始化,且不能赋值,这个与之前的const属性完全一致。

  2. 引用数据成员

    必须要在初始化列表中进行初始化,并且占一个指针大小空间,不能赋值,否则报错。

  3. 类数据成员

    默认情况下,类对象数据成员也会初始化列表中进行初始化,但是此时只会调用子对象的默认构造函数,如果默认构造函数没有,会报错,所以为了达到要求,最好在初始化列表中进行显示初始化。

  4. 静态数据成员

    不能要在初始化列表中进行初始化,要放在全局静态的位置进行初始化,并且对于具有头文件与实现文件的形式,必须在实现文件中进行,否则会出现重定义问题。但是静态数据成员不占用类的大小(严格说是类所创建的对象的大小,因为静态数据成员数被类创建的所有对象共享的)。格式如下:

    类型 类名::变量名 = 初始化表达式; //普通变量

    类型 类名::对象名(构造参数); //对象变量


七.特殊成员函数

  1. 静态成员函数

    1. 静态的成员函数的第一个参数位置没有this指针
    2. 静态的成员函数不能访问非静态的数据成员和非静态的成员函数,无this指针,不能分辨是哪个对象数据成员或者成员函数。
    3. 非静态的成员函数能访问静态的数据成员和静态的成员函数
    4. 静态成员函数可以使用类名加域作用符的形式进行调用(静态成员函数的特殊用法)
    5. 用某个类类型的空指针也可以调用该类的静态成员函数和未访问对象成员的成员函数。
  2. const成员函数

    1. 对于非const对象,既可以调用const版本的成员函数,可以调用非const版本的成员函数,默认情况下,调用非const版本的成员函数 ;
    2. const对象调用const版本的成员函数,不能调用非const版本的函数;
    3. const版本的成员函数与非const版本的成员函数可以重载,一般先写const版本的,重载的原因是隐藏的this指针的类型不一样,const Point * const this和 Point * const this;
    4. const版本的成员函数具有只读特性,不能进行写操作,所以对于不修改数据成员的情况,可以将成员函数用const修饰。

八.对象的组织

  1. const对象

  2. 指向对象的指针

  3. 对象数组

    可以在声明时进行初始化,且可以简写(有点神奇):

    Point pts[2] = {Point(1,2),Point(3,4)};
    Point pts[] = {Point(1,2),Point(3,4)};
    Point pts[5] = {Point(1,2),Point(3,4)};
    //或者
    Point pts[2] = {{12}{34}};
    Point pts[] = {{12}{34}};
    Point pts[5] = {{12}{34}};
    
  4. 堆对象

    Point *pt2 = new Point[5]();
    pt2->print();
    (pt2+1)->print();
    
    delete [] pt2;
    

    利用堆创建数组时会自动调用构造函数,调用次数跟数组大小一致。


九.单例模式

  1. 设计需求:一个类只能创建一个对象

  2. 应用场景:全局唯一的资源,日志记录器、网页库、字典库

  3. 实现步骤:

    1. 构造函数设计为私有
    2. 设置静态的成员函数(getInstance)
    3. 静态数据成员_pInstance
    4. 将析构函数设置为私有(防止用户自己多次delete,造成double free)
    5. 为了释放堆空间,设计静态的析构函数(destroy)
    class Singleton
    {
    public:
        static Singleton *getInstance()
        {
            if(nullptr == _pInstance)
            {
                _pInstance = new Singleton();
            }
            return _pInstance;
        }
        static void destroy()
        {
            if(_pInstance)
            {
                delete _pInstance;
                _pInstance = nullptr;
            }
        }
    private:
        Singleton()
        {
            cout << "Singleton()" << endl;
        }
        ~Singleton()
        {
            cout << "~Singleton()" << endl;
        }
    private:
        static Singleton *_pInstance;
    };
    
    Singleton *Singleton::_pInstance =  nullptr;
    

十.new与delete表达式

  1. new表达式的工作步骤

    1. 调用名为operator new的标准库函数,分配足够大的原始的未类型化的内存,以保存指定类型的一个对象 ;
    2. 运行该类型的一个构造函数初始化对象 ;
    3. 返回指向新分配并构造的构造函数对象的指针 。
  2. delete表达式的工作步骤

    1. 调用析构函数,回收对象中数据成员所申请的资源 ;
    2. 调用名为operator delete的标准库函数释放该对象所用的内存。
  3. 函数

    //operator new库函数
    void *operator new(size_t);
    void *operator new[](size_t);
    
    //operator delete库函数
    void operator delete(void *);
    void operator delete(void *);
    
    //这两类函数底层还是malloc和free
    

    这几个函数都是静态成员函数(static),无this指针,operator new时还未初始化对象,肯定没有this指针。可以将这些函数放到类外作为公共函数,这时候函数将作用于所有的new和delete表达式。

  4. 重要问题

    1. 对象的销毁与析构函数的调用是不是等价的?

      对于栈对象,本句话是对的,但是对于堆对象而言,析构函数的的调用只是对象销毁中的一个步骤,接着还会调用operator delete。

    2. 创建栈对象的条件是什么?

      构造函数与析构函数都要是公有的(public)

  5. 要求一个类只能创建栈对象:

    解决方法:将operator new函数变成私有成员函数即可;

    (因为创建堆对象时第一步就是调用operator new函数申请存放对象的空间,此时类外无法调用该函数,自然不能创建堆对象)。

  6. 要求一个类只能创建堆对象:

    解决方法:将该类的析构函数放入private区域,此时不能创建栈对象,但是堆对象的销毁要专门写个destory()函数,在该函数里delete this。


十一.内存对齐

  1. 数据成员对齐规则

    结构(struct)(或联合(union))的数据成员,第一个数据成员放在offset(偏移)为0的地方,以后每个数据成员的对齐按照 #pragma pack 指定的数值和这个数据成员自身长度中,比较小的那个进行。

  2. 结构(联合)的整体对齐规则

    在数据成员完成各自对齐之后,结构(或联合)本身也要进行对齐,对齐将按照 #pragma pack 指定的数值和结构(或联合)最大数据成员长度中,比较小的那个进行。

  3. 结构体作为成员

    如果一个结构里有某些结构体成员,则内部结构体成员要从成员最大元素大小的整数倍和#pragma pack指定的数值中最小的一个的整数倍的地址开始存储。

    #pragma pack(n) 对齐系数

或:

  1. 对于结构体的各个成员,第一个成员的偏移量是0,排列在后面的成员其当前偏移量必须是当前成员类型的整数倍。

  2. 结构体内所有数据成员各自内存对齐后,结构体本身还要进行一次内存对齐,保证整个结构体占用内存大小是结构体内最大数据成员的最小整数倍。

  3. 如程序中有#pragma pack(n)预编译指令,则所有成员对齐以n字节为准(即偏移量是n的整数倍),不再考虑当前类型以及最大结构体内类型。


十二.相关题目

  1. 定义一个空的类型,里面没有任何成员变量和成员函数。对该类型求sizeof,得到的结果时多少?在该类中添加构造函数和析构函数,再对该类型求sizeof,得到的结果时多少?如果析构函数标记为虚函数呢?再对该类型求sizeof,得到的结果时多少?

    解:

    第一问:答案是1B,而不是0B。我们在声明该类型实例的时候,必须给实例在内存中分配一定的空间,否则无法使用该实例。由于空类型不含任何信息,故而所占的内存大小由编译器决定。codeblocks和Visual Studio中每个空类型的实例占1B。切忌:一旦类中有其他的占用空间成员,则这1个字节就不在计算之内。

    第二问:在该类中添加构造函数和析构函数,再对该类型求sizeof,结果仍未1B。因为成员函数只与类型相关,而与具体实例无关。进一步引申:如果有其他成员函数(非虚函数),则还是只占用1个字节。

    第三问:C++编译器一旦发现类型中有虚函数,就会为该类型生成虚函数表,并在该类型的每个实例中添加一个指向虚函数表的指针。在32位的机器上,一个指针占4B;在64位的机器上,一个指针占8B。为何需要虚函数表?因为虚函数表是C++实现多态的一种机制。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值